Marcin Wardyński
Wtorek, 9:45

Uczenie Maszynowe - LAB2b - LIME¶

Biblioteka LIME: https://github.com/marcotcr/lime (Dokumentacja API: https://lime-ml.readthedocs.io/en/latest/)

Wprowadzenie - pakiety¶

Niezbędne pakiety i moduły na potrzeby wprowadzenia

In [1]:
import json
from functools import partial

import matplotlib.pyplot as plt
import numpy as np
import torch
from lime import lime_image
from PIL import Image
from skimage.segmentation import mark_boundaries
from torchvision import models, transforms
/Users/mwardynski/Documents/ds/_semestr_9/uczenie_maszynowe/labs/.venv/lib/python3.12/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html
  from .autonotebook import tqdm as notebook_tqdm

Wprowadzenie - funkcje pomocnicze¶

Funkcja do wczytywania wskazanego obrazka oraz konwersji do palety RGB.

In [2]:
def get_image(path):
    with open(path, 'rb') as f:
        with Image.open(f) as img:
            return img.convert('RGB')

Funkcja do przekształcania obrazka (zwróconego przez funkcję get_image) w tensor, akceptowalny na wejściu sieci neronowej.

In [5]:
def image_to_tensor(img):    
    transformer = transforms.Compose([
        transforms.Resize((256, 256)),
        transforms.CenterCrop(224),
        transforms.ToTensor(),
        transforms.Normalize(
            mean=[0.485, 0.456, 0.406],
            std=[0.229, 0.224, 0.225])
    ])  
    return transformer(img).unsqueeze(0)

Funkcja używana przez LIME, przyjmuje na wejściu zbiór obrazków, a zwraca prawdopodobieństwa klas. Należy ją przekazać do lime_image.LimeImageExplainer().explain_instance przy użyciu partial, jako partial(predict_batch, <model>), gdzie modelem w naszym wypadku będą sieci neuronowe. Przykłady użycia są zawarte w tym notebooku.

In [6]:
def predict_batch(model, images):
    model.eval()
    transformer = transforms.Compose([
        transforms.ToTensor(),
        transforms.Normalize(
            mean=[0.485, 0.456, 0.406],
            std=[0.229, 0.224, 0.225]) 
    ])   
    
    model.eval()
    batch = torch.stack(tuple(transformer(i) for i in images), dim=0)

    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    model.to(device)
    batch = batch.to(device)
    
    logits = model(batch)
    probas = torch.nn.functional.softmax(logits, dim=1)
    return probas.detach().cpu().numpy()

Funkcja, która przekształca obrazek w format akceptowany na wejściu przez LIME. Przykłady użycia są zawarte w tym notebooku.

In [7]:
def lime_transformer(image):
    transformer = transforms.Compose([
        transforms.Resize((256, 256)),
        transforms.CenterCrop(224)
    ])
    return np.array(transformer(image))

LIME jest głównie wykorzystywane do wyjaśniania predykcji tzw. czarnych skrzynek, czyli modeli nieinterpretowalnych. Idealnymi kandydatami są Głębokie Sieci Neuronowe, dlatego spróbujemy wyjaśnić niektóre predykcje gotowych modeli.

Model Inception-v3 - przygotowanie danych¶

https://arxiv.org/abs/1512.00567

Plik ./data/imagenet_class_index.json zawiera przypisanie klas obrazków do indeksów. Jest to istotne, ponieważ zwracane wyniki (np. wartości funkcji logit na wyjściu sieci neuronowych) wykorzystują to, zwracając wyniki w zadanej kolejności.

In [8]:
with open("./data/imagenet_class_index.json") as f:
    content = json.load(f)
    index_to_label = {
        int(index): data[1]
        for index, data in content.items()
    }
In [9]:
image_to_classify = get_image("./data/dogs.png")
plt.imshow(image_to_classify)
Out[9]:
<matplotlib.image.AxesImage at 0x1299cc290>
No description has been provided for this image
In [10]:
img_tensor = image_to_tensor(image_to_classify)

Załadowanie pretrenowanego modelu¶

In [11]:
inception_v3 = models.inception_v3(pretrained=True)
/Users/mwardynski/Documents/ds/_semestr_9/uczenie_maszynowe/labs/.venv/lib/python3.12/site-packages/torchvision/models/_utils.py:208: UserWarning: The parameter 'pretrained' is deprecated since 0.13 and may be removed in the future, please use 'weights' instead.
  warnings.warn(
/Users/mwardynski/Documents/ds/_semestr_9/uczenie_maszynowe/labs/.venv/lib/python3.12/site-packages/torchvision/models/_utils.py:223: UserWarning: Arguments other than a weight enum or `None` for 'weights' are deprecated since 0.13 and may be removed in the future. The current behavior is equivalent to passing `weights=Inception_V3_Weights.IMAGENET1K_V1`. You can also use `weights=Inception_V3_Weights.DEFAULT` to get the most up-to-date weights.
  warnings.warn(msg)
Downloading: "https://download.pytorch.org/models/inception_v3_google-0cc3c7bd.pth" to /Users/mwardynski/.cache/torch/hub/checkpoints/inception_v3_google-0cc3c7bd.pth
100%|██████████| 104M/104M [00:01<00:00, 74.6MB/s] 

Predykcja¶

In [12]:
inception_v3.eval()
logits = inception_v3(img_tensor)

Zwróć uwagę, że model zwraca wartości funkcji logit, a nie prawdopodobieństwa klas, dlatego wyniki trzeba przetworzyć (np. przy użyciu funkcji softmax).

In [13]:
probas = torch.nn.functional.softmax(logits, dim=1)

Sprawdźmy N najbardziej prawdopodobnych klas

In [14]:
TOP_N_LABELS = 15

probas_top = probas.topk(TOP_N_LABELS)
top_probas = probas_top[0][0].detach().numpy()
top_labels = probas_top[1][0].detach().numpy()
for proba, label in zip(top_probas, top_labels):
    print(f"Class: {index_to_label[label]:<30} | Probability: {proba:.6f}")
Class: Bernese_mountain_dog           | Probability: 0.935930
Class: EntleBucher                    | Probability: 0.038448
Class: Appenzeller                    | Probability: 0.023756
Class: Greater_Swiss_Mountain_dog     | Probability: 0.001818
Class: Gordon_setter                  | Probability: 0.000009
Class: Blenheim_spaniel               | Probability: 0.000007
Class: English_springer               | Probability: 0.000002
Class: tabby                          | Probability: 0.000002
Class: robin                          | Probability: 0.000001
Class: guinea_pig                     | Probability: 0.000001
Class: amphibian                      | Probability: 0.000001
Class: Japanese_spaniel               | Probability: 0.000001
Class: African_grey                   | Probability: 0.000001
Class: Brittany_spaniel               | Probability: 0.000001
Class: toucan                         | Probability: 0.000001

Teraz możemy te funkcje zebrać razem¶

In [15]:
def get_prediction_probabilities(image, model):
    img_tensor = image_to_tensor(image)
    model.eval()
    logits = model(img_tensor)
    probas = torch.nn.functional.softmax(logits, dim=1)
    
    TOP_N_LABELS = 15

    probas_top = probas.topk(TOP_N_LABELS)
    top_probas = probas_top[0][0].detach().numpy()
    top_labels = probas_top[1][0].detach().numpy()
    for proba, label in zip(top_probas, top_labels):
        print(f"Class: {index_to_label[label]:<30} | Probability: {proba:.6f}")

I sprawdzić jak ta predykcja wygląda dla innego obrazka¶

In [16]:
exercise_image = get_image("./data/cat_mouse.jpeg")
plt.imshow(exercise_image)
Out[16]:
<matplotlib.image.AxesImage at 0x12a90da90>
No description has been provided for this image

Zadanie: sprawdź jak będzie wyglądała predykcja dla powyższego obrazka¶

In [17]:
get_prediction_probabilities(exercise_image, inception_v3)
Class: Egyptian_cat                   | Probability: 0.967492
Class: tabby                          | Probability: 0.024167
Class: lynx                           | Probability: 0.005490
Class: tiger_cat                      | Probability: 0.002165
Class: Persian_cat                    | Probability: 0.000105
Class: Angora                         | Probability: 0.000074
Class: swab                           | Probability: 0.000071
Class: Madagascar_cat                 | Probability: 0.000064
Class: snow_leopard                   | Probability: 0.000040
Class: tile_roof                      | Probability: 0.000037
Class: indri                          | Probability: 0.000020
Class: leopard                        | Probability: 0.000016
Class: Siamese_cat                    | Probability: 0.000011
Class: ram                            | Probability: 0.000010
Class: crate                          | Probability: 0.000009

Model Inception-v3 - wyjaśnienie¶

Chcemy wiedzieć dlaczego klasa Bernese_mountain_dog została uznana przez sieć neuronową za najbardziej prawdopodobną (to znaczy - które piksele obrazka o tym zadecydowały). W tym celu właśnie wykorzystamy LIME.

W jaki sposób działa LIME na obrazkach?

  1. Na wejściu wymagany jest oryginalny obrazek.
  2. Wejściowy obrazek jest delikatnie przekształcany wiele razy, dzięki czemu otrzymujemy wiele podobnych (ale nie takich samych!) obrazków.
  3. Dodatkowo na wejście musimy podać funkcję, która każdemu takiemu przekształceniu nada prawdopodobieństwo przynależności do danej klasy. Jest to wymagane ponieważ LIME jest niezależny od żadnych narzędzi i modeli.
In [18]:
explainer = lime_image.LimeImageExplainer()
In [19]:
explanation = explainer.explain_instance(
    image=lime_transformer(image_to_classify), 
    classifier_fn=partial(predict_batch, inception_v3),
    top_labels=5,
    num_samples=1000)
100%|██████████| 1000/1000 [00:27<00:00, 35.85it/s]

Mając te dane możemy teraz sprawdzić które kategorie są najbardziej prawdopodobne

In [20]:
for index in explanation.top_labels:
    print(index_to_label[index])
Bernese_mountain_dog
EntleBucher
Appenzeller
Greater_Swiss_Mountain_dog
Gordon_setter

Zobaczmy co wpłynęło na wybranie Bernese_mountain_dog jako najbardziej prawdopodobnej klasy.

In [37]:
image, mask = explanation.get_image_and_mask(
    label=explanation.top_labels[0],
    positive_only=False,
    negative_only=False,
    num_features=10,
    hide_rest=False)
boundaries = mark_boundaries(image, mask)
plt.imshow(boundaries)
Out[37]:
<matplotlib.image.AxesImage at 0x12fb19130>
No description has been provided for this image

Zadanie: zmień wartość NUM_FEATURES i zaobserwuj jak zmienia się mapowanie¶

NUM_FEATURES najlepiej zmieniać w zakresie 1:50

In [55]:
NUM_FEATURES = 50

image, mask = explanation.get_image_and_mask(
    label=explanation.top_labels[0],
    positive_only=False,
    negative_only=False,
    num_features=NUM_FEATURES,
    hide_rest=False)
boundaries = mark_boundaries(image, mask)
plt.imshow(boundaries)
Out[55]:
<matplotlib.image.AxesImage at 0x12c255f40>
No description has been provided for this image

Zielone fragmenty oznaczają "superpiksele", które pozytywnie wpływają na predykowaną klasę. Czerwone fragmenty wpływają negatywnie.

Zadanie-pytanie: co to właściwie jest superpiksel?¶

Zadanie-pytanie: czy jeden superpiksel ma odzwierciedlenie w jednym pikselu z obrazka?¶

Superpiksel to zbiór umieszczonych blisko siebie piskeli, które współdzielą pewne właściwości, jak np. kolor. Superpiksel nie ma odzwierciedlenia w pojedyńczym pikselu z obrazka, to piksele z obrazka mają odzwierciedlenie w superpikselu.

Zobaczmy jak to się prezentuje dla drugiej najbardziej prawdopodobnej klasy, czyli EntleBucher, która jednak otrzymała jedyne 3.8%.

In [25]:
image, mask = explanation.get_image_and_mask(
    label=explanation.top_labels[1],
    positive_only=False,
    negative_only=False,
    num_features=10,
    hide_rest=False)
boundaries = mark_boundaries(image, mask)
plt.imshow(boundaries)
Out[25]:
<matplotlib.image.AxesImage at 0x12ab6deb0>
No description has been provided for this image

Ustawiając wartości hide_rest oraz positive_only na True jesteśmy w stanie zostawić tylko te piksele, które potwierdzały przynależność do danej klasy
Musimy jednak pamiętać o przeskalowaniu rezultatu przy pomocy (boundaries).astype(np.uint8)

In [26]:
image, mask = explanation.get_image_and_mask(
    label=explanation.top_labels[0],
    positive_only=True,
    negative_only=False,
    num_features=10,
    hide_rest=True)
boundaries = mark_boundaries(image, mask)
plt.imshow((boundaries).astype(np.uint8))
Out[26]:
<matplotlib.image.AxesImage at 0x12acd0fb0>
No description has been provided for this image

Możemy również zostawić tylko te piksele, które zaprzeczały przynależności do danej klasy

In [57]:
image, mask = explanation.get_image_and_mask(
    label=explanation.top_labels[0],
    positive_only=False,
    negative_only=True,
    num_features=10,
    hide_rest=True)
boundaries = mark_boundaries(image, mask)
cropped_image_ndarray = (boundaries).astype(np.uint8)
plt.imshow(cropped_image_ndarray)
Out[57]:
<matplotlib.image.AxesImage at 0x139f27980>
No description has been provided for this image

A następnie sprawdzić co model sądzi o tak wyciętym obrazku

In [28]:
cropped_image_pil = Image.fromarray(cropped_image_ndarray)

get_prediction_probabilities(cropped_image_pil, inception_v3)
Class: feather_boa                    | Probability: 0.459048
Class: groom                          | Probability: 0.445271
Class: mortarboard                    | Probability: 0.034262
Class: kimono                         | Probability: 0.014381
Class: bow_tie                        | Probability: 0.007375
Class: academic_gown                  | Probability: 0.005310
Class: abaya                          | Probability: 0.004244
Class: suit                           | Probability: 0.002887
Class: military_uniform               | Probability: 0.001675
Class: grand_piano                    | Probability: 0.001089
Class: theater_curtain                | Probability: 0.001022
Class: limousine                      | Probability: 0.000963
Class: gown                           | Probability: 0.000759
Class: cornet                         | Probability: 0.000723
Class: wig                            | Probability: 0.000655

I jak go teraz widzi model

In [29]:
cropped_image_explanation = explainer.explain_instance(
    image=lime_transformer(cropped_image_pil), 
    classifier_fn=partial(predict_batch, inception_v3),
    top_labels=5,
    num_samples=1000)
100%|██████████| 1000/1000 [00:29<00:00, 34.18it/s]
In [30]:
image, mask = cropped_image_explanation.get_image_and_mask(
    label=cropped_image_explanation.top_labels[0],
    positive_only=False,
    negative_only=False,
    num_features=10,
    hide_rest=False)
boundaries = mark_boundaries(image, mask)
plt.imshow(boundaries)
Out[30]:
<matplotlib.image.AxesImage at 0x12ad831d0>
No description has been provided for this image

Model Inception-v3 - porównanie z AlexNet¶

Przetestujmy działanie na innym modelu - AlexNet

In [38]:
alexnet = models.alexnet(pretrained=True)
/Users/mwardynski/Documents/ds/_semestr_9/uczenie_maszynowe/labs/.venv/lib/python3.12/site-packages/torchvision/models/_utils.py:208: UserWarning: The parameter 'pretrained' is deprecated since 0.13 and may be removed in the future, please use 'weights' instead.
  warnings.warn(
/Users/mwardynski/Documents/ds/_semestr_9/uczenie_maszynowe/labs/.venv/lib/python3.12/site-packages/torchvision/models/_utils.py:223: UserWarning: Arguments other than a weight enum or `None` for 'weights' are deprecated since 0.13 and may be removed in the future. The current behavior is equivalent to passing `weights=AlexNet_Weights.IMAGENET1K_V1`. You can also use `weights=AlexNet_Weights.DEFAULT` to get the most up-to-date weights.
  warnings.warn(msg)
Downloading: "https://download.pytorch.org/models/alexnet-owt-7be5be79.pth" to /Users/mwardynski/.cache/torch/hub/checkpoints/alexnet-owt-7be5be79.pth
100%|██████████| 233M/233M [00:02<00:00, 101MB/s]  
In [39]:
explanation_alexnet = explainer.explain_instance(
    image=lime_transformer(image_to_classify), 
    classifier_fn=partial(predict_batch, alexnet),
    top_labels=5,
    num_samples=1000)
100%|██████████| 1000/1000 [00:09<00:00, 108.49it/s]
In [40]:
for index_alex, index_inception in zip(explanation_alexnet.top_labels, explanation.top_labels):
    print(f"{index_to_label[index_alex]:30} | {index_to_label[index_inception]:30}")
Bernese_mountain_dog           | Bernese_mountain_dog          
EntleBucher                    | EntleBucher                   
Greater_Swiss_Mountain_dog     | Appenzeller                   
Appenzeller                    | Greater_Swiss_Mountain_dog    
basset                         | Gordon_setter                 

Jak widać, klasy nieco się różnią, ale TOP 1 pozostaje takie samo.

In [41]:
image, mask = explanation_alexnet.get_image_and_mask(
    label=explanation_alexnet.top_labels[0],
    positive_only=False,
    negative_only=False,
    num_features=10,
    hide_rest=False)
boundaries = mark_boundaries(image, mask)
plt.imshow(boundaries)
Out[41]:
<matplotlib.image.AxesImage at 0x12fd07ef0>
No description has been provided for this image

Wyjaśnienie dla AlexNet jak się można było spodziewać - też się różni, jednak w dalszym ciągu do klasyfikacji psa istotny jest... pies :)

Zadanie: porównaj predykcje obrazka dla modeli inception_v3 oraz alexnet¶

In [43]:
print("inception_v3")
get_prediction_probabilities(exercise_image, inception_v3)
print()
print("alexnet")
get_prediction_probabilities(exercise_image, alexnet)
inception_v3
Class: Egyptian_cat                   | Probability: 0.967492
Class: tabby                          | Probability: 0.024167
Class: lynx                           | Probability: 0.005490
Class: tiger_cat                      | Probability: 0.002165
Class: Persian_cat                    | Probability: 0.000105
Class: Angora                         | Probability: 0.000074
Class: swab                           | Probability: 0.000071
Class: Madagascar_cat                 | Probability: 0.000064
Class: snow_leopard                   | Probability: 0.000040
Class: tile_roof                      | Probability: 0.000037
Class: indri                          | Probability: 0.000020
Class: leopard                        | Probability: 0.000016
Class: Siamese_cat                    | Probability: 0.000011
Class: ram                            | Probability: 0.000010
Class: crate                          | Probability: 0.000009

alexnet
Class: Persian_cat                    | Probability: 0.449892
Class: Egyptian_cat                   | Probability: 0.105569
Class: hamster                        | Probability: 0.075303
Class: lynx                           | Probability: 0.069438
Class: tiger_cat                      | Probability: 0.047823
Class: Angora                         | Probability: 0.043763
Class: tabby                          | Probability: 0.021709
Class: bucket                         | Probability: 0.020403
Class: plastic_bag                    | Probability: 0.013197
Class: coffee_mug                     | Probability: 0.012102
Class: tub                            | Probability: 0.011971
Class: hare                           | Probability: 0.008684
Class: hamper                         | Probability: 0.007576
Class: saltshaker                     | Probability: 0.005267
Class: wood_rabbit                    | Probability: 0.005078

Jak widzimy powyżej, dla obrazka ćwiczeniowego Alexnet oraz Inception_v3 proponują inne rozwiązania. Zaproponowany z niemalże pewnością przez Inception_v3 "Egyptian_cat" jest na miejscu drugim w Alexnet, ale ze znaczącą stratą do pierwszej klasy, czyli "Persian_cat", który to znowu jest dopiero na miejscu piątym w Inception_v3.

Zadanie domowe - wstęp¶

W folderze data znajduje się zdjęcie amfibii: title

In [158]:
amphibious_vehicle = get_image("./data/amfibia.jpg")
In [159]:
inception_v3 = models.inception_v3(pretrained=True)

explanation_amhibious_vehicle_inception_v3 = explainer.explain_instance(
    image=lime_transformer(amphibious_vehicle), 
    classifier_fn=partial(predict_batch, inception_v3),
    top_labels=5,
    num_samples=1000)

image, mask = explanation_amhibious_vehicle_inception_v3.get_image_and_mask(
    label=explanation_amhibious_vehicle_inception_v3.top_labels[0],
    positive_only=False,
    negative_only=False,
    num_features=10,
    hide_rest=False)
boundaries = mark_boundaries(image, mask)
plt.imshow(boundaries)
/Users/mwardynski/Documents/ds/_semestr_9/uczenie_maszynowe/labs/.venv/lib/python3.12/site-packages/torchvision/models/_utils.py:208: UserWarning: The parameter 'pretrained' is deprecated since 0.13 and may be removed in the future, please use 'weights' instead.
  warnings.warn(
/Users/mwardynski/Documents/ds/_semestr_9/uczenie_maszynowe/labs/.venv/lib/python3.12/site-packages/torchvision/models/_utils.py:223: UserWarning: Arguments other than a weight enum or `None` for 'weights' are deprecated since 0.13 and may be removed in the future. The current behavior is equivalent to passing `weights=Inception_V3_Weights.IMAGENET1K_V1`. You can also use `weights=Inception_V3_Weights.DEFAULT` to get the most up-to-date weights.
  warnings.warn(msg)
100%|██████████| 1000/1000 [00:29<00:00, 34.07it/s]
Out[159]:
<matplotlib.image.AxesImage at 0x13853d310>
No description has been provided for this image

Model inception_v3 jak i jego wyjaśnienie rzeczywiście sugerują amfibię jako najbardziej prawdopodobną klasę:

In [46]:
for index in explanation_amhibious_vehicle_inception_v3.top_labels:
    print(index_to_label[index])
amphibian
convertible
racer
car_wheel
golfcart

Zadanie #1¶

Użyj dwóch różnych sieci neuronowych (poza inception_v3, którego przykład jest powyżej) do wygenerowania wyjaśnień.
(skorzystaj z modułu torchvision: https://pytorch.org/vision/stable/models.html)

In [47]:
mobilenet_v3_l = models.mobilenet_v3_large(pretrained=True)

explanation_amhibious_vehicle_mobilenet_v3_l = explainer.explain_instance(
    image=lime_transformer(amphibious_vehicle), 
    classifier_fn=partial(predict_batch, mobilenet_v3_l),
    top_labels=5,
    num_samples=1000)

image, mask = explanation_amhibious_vehicle_mobilenet_v3_l.get_image_and_mask(
    label=explanation_amhibious_vehicle_mobilenet_v3_l.top_labels[0],
    positive_only=False,
    negative_only=False,
    num_features=10,
    hide_rest=False)
boundaries = mark_boundaries(image, mask)
plt.imshow(boundaries)
/Users/mwardynski/Documents/ds/_semestr_9/uczenie_maszynowe/labs/.venv/lib/python3.12/site-packages/torchvision/models/_utils.py:208: UserWarning: The parameter 'pretrained' is deprecated since 0.13 and may be removed in the future, please use 'weights' instead.
  warnings.warn(
/Users/mwardynski/Documents/ds/_semestr_9/uczenie_maszynowe/labs/.venv/lib/python3.12/site-packages/torchvision/models/_utils.py:223: UserWarning: Arguments other than a weight enum or `None` for 'weights' are deprecated since 0.13 and may be removed in the future. The current behavior is equivalent to passing `weights=MobileNet_V3_Large_Weights.IMAGENET1K_V1`. You can also use `weights=MobileNet_V3_Large_Weights.DEFAULT` to get the most up-to-date weights.
  warnings.warn(msg)
Downloading: "https://download.pytorch.org/models/mobilenet_v3_large-8738ca79.pth" to /Users/mwardynski/.cache/torch/hub/checkpoints/mobilenet_v3_large-8738ca79.pth
100%|██████████| 21.1M/21.1M [00:00<00:00, 70.9MB/s]
100%|██████████| 1000/1000 [00:33<00:00, 29.70it/s]
Out[47]:
<matplotlib.image.AxesImage at 0x13ac0e000>
No description has been provided for this image
In [48]:
for index in explanation_amhibious_vehicle_mobilenet_v3_l.top_labels:
    print(index_to_label[index])
amphibian
racer
tow_truck
convertible
car_wheel
In [172]:
resnet50 = models.resnet50(pretrained=True)

explanation_amhibious_vehicle_resnet50 = explainer.explain_instance(
    image=lime_transformer(amphibious_vehicle), 
    classifier_fn=partial(predict_batch, resnet50),
    top_labels=5,
    num_samples=1000)

image, mask = explanation_amhibious_vehicle_resnet50.get_image_and_mask(
    label=explanation_amhibious_vehicle_resnet50.top_labels[0],
    positive_only=False,
    negative_only=False,
    num_features=10,
    hide_rest=False)
boundaries = mark_boundaries(image, mask)
plt.imshow(boundaries)
/Users/mwardynski/Documents/ds/_semestr_9/uczenie_maszynowe/labs/.venv/lib/python3.12/site-packages/torchvision/models/_utils.py:208: UserWarning: The parameter 'pretrained' is deprecated since 0.13 and may be removed in the future, please use 'weights' instead.
  warnings.warn(
/Users/mwardynski/Documents/ds/_semestr_9/uczenie_maszynowe/labs/.venv/lib/python3.12/site-packages/torchvision/models/_utils.py:223: UserWarning: Arguments other than a weight enum or `None` for 'weights' are deprecated since 0.13 and may be removed in the future. The current behavior is equivalent to passing `weights=ResNet50_Weights.IMAGENET1K_V1`. You can also use `weights=ResNet50_Weights.DEFAULT` to get the most up-to-date weights.
  warnings.warn(msg)
100%|██████████| 1000/1000 [00:30<00:00, 32.62it/s]
Out[172]:
<matplotlib.image.AxesImage at 0x12b865310>
No description has been provided for this image
In [51]:
for index in explanation_amhibious_vehicle_resnet50.top_labels:
    print(index_to_label[index])
amphibian
racer
car_wheel
sports_car
pickup

Jak widzimy powyżej, każdy z modeli zaznacza trochę inny zestaw superpiskeli jako ten najbardziej naprowadzający na daną klasę pojazdu, jednakże widać dużą zbieżność pomiędzy superpikselami zaznaczonymi przez każdy z modeli.

Wszystkie modele skutecznie zaklasyfikowały pojazd jako amfibię.

Zadanie #2¶

Zmodyfikuj oryginalny obrazek w taki sposób, żeby najbardziej prawdopodobną klasą dla każdej z tych sieci nie była amfibia a jakiś inny pojazd (np. samochód). W tym celu możesz "zasłonić" czarnym kwadratem (wartość 0 w macierzy reprezentującej obraz) obszary istotne przy klasyfikacji.
Przydatną rzeczą będzie skorzystanie z opcji hide_rest w funkcji get_image_and_mask i późniejsza obróbka obrazu

In [222]:
from PIL import Image, ImageDraw

def calculate_rectagle_covering(mask):
    width = mask.shape[0]
    height = mask.shape[1]
    min_x, min_y = width, height
    max_x, max_y = 0, 0

    for x in range(width):
        for y in range(height):
            px = mask[y][x]
            
            if px == 1:
                if x < min_x:
                    min_x = x
                if y < min_y:
                    min_y = y
                if x > max_x:
                    max_x = x
                if y > max_y:
                    max_y = y
    return (min_x, min_y), (max_x, max_y)

def cover_image_with_rectangle(mask, image):
    rectangle = calculate_rectagle_covering(mask)
    
    lime_image = lime_transformer(image)
    lime_image[rectangle[0][1]:rectangle[1][1]+1, rectangle[0][0]:rectangle[1][0]+1] = [0, 0, 0]
    return lime_image
In [223]:
num_features=1

image, mask = explanation_amhibious_vehicle_inception_v3.get_image_and_mask(
    label=explanation_amhibious_vehicle_inception_v3.top_labels[0],
    positive_only=True,
    negative_only=False,
    num_features=num_features,
    hide_rest=True)

covered_lime_image = cover_image_with_rectangle(mask, amphibious_vehicle)

explanation_c_amhibious_vehicle_inception_v3 = explainer.explain_instance(
    image=covered_lime_image, 
    classifier_fn=partial(predict_batch, inception_v3),
    top_labels=5,
    num_samples=1000)

image, mask = explanation_c_amhibious_vehicle_inception_v3.get_image_and_mask(
    label=explanation_c_amhibious_vehicle_inception_v3.top_labels[0],
    positive_only=False,
    negative_only=False,
    num_features=10,
    hide_rest=False)
boundaries = mark_boundaries(image, mask)
plt.imshow(boundaries)

for index in explanation_c_amhibious_vehicle_inception_v3.top_labels:
    print(index_to_label[index])
100%|██████████| 1000/1000 [00:28<00:00, 35.31it/s]
tow_truck
amphibian
pickup
forklift
harvester

No description has been provided for this image
In [226]:
num_features=1

image, mask = explanation_amhibious_vehicle_mobilenet_v3_l.get_image_and_mask(
    label=explanation_amhibious_vehicle_inception_v3.top_labels[0],
    positive_only=True,
    negative_only=False,
    num_features=num_features,
    hide_rest=True)

covered_lime_image = cover_image_with_rectangle(mask, amphibious_vehicle)

explanation_c_amhibious_vehicle_mobilenet_v3_l = explainer.explain_instance(
    image=covered_lime_image, 
    classifier_fn=partial(predict_batch, mobilenet_v3_l),
    top_labels=5,
    num_samples=1000)

image, mask = explanation_c_amhibious_vehicle_mobilenet_v3_l.get_image_and_mask(
    label=explanation_c_amhibious_vehicle_mobilenet_v3_l.top_labels[0],
    positive_only=False,
    negative_only=False,
    num_features=10,
    hide_rest=False)
boundaries = mark_boundaries(image, mask)
plt.imshow(boundaries)

for index in explanation_c_amhibious_vehicle_mobilenet_v3_l.top_labels:
    print(index_to_label[index])
100%|██████████| 1000/1000 [00:27<00:00, 35.90it/s]
tow_truck
amphibian
racer
pickup
tractor

No description has been provided for this image
In [229]:
num_features=3

image, mask = explanation_amhibious_vehicle_resnet50.get_image_and_mask(
    label=explanation_amhibious_vehicle_inception_v3.top_labels[0],
    positive_only=True,
    negative_only=False,
    num_features=num_features,
    hide_rest=True)

covered_lime_image = cover_image_with_rectangle(mask, amphibious_vehicle)

explanation_c_amhibious_vehicle_resnet50 = explainer.explain_instance(
    image=covered_lime_image, 
    classifier_fn=partial(predict_batch, resnet50),
    top_labels=5,
    num_samples=1000)

image, mask = explanation_c_amhibious_vehicle_resnet50.get_image_and_mask(
    label=explanation_c_amhibious_vehicle_resnet50.top_labels[0],
    positive_only=False,
    negative_only=False,
    num_features=10,
    hide_rest=False)
boundaries = mark_boundaries(image, mask)
plt.imshow(boundaries)

for index in explanation_c_amhibious_vehicle_resnet50.top_labels:
    print(index_to_label[index])
100%|██████████| 1000/1000 [00:29<00:00, 33.61it/s]
wreck
tractor
pickup
amphibian
tow_truck

No description has been provided for this image

Widzimy powyżej, że wystarczyło przykryć czarnym prostokątem obszar zawierający najbardziej znaczący superpiksel przy klasyfikacji modelami InceptionV3 oraz MobileNetV3, żeby je zwieść i otrzymać klasyfikację "tow_truck"

Model ResNet wykazał się największą odpornością i dopiero po przykryciu trzech najistotniejszych dla klasyfikacji superpikseli, bez których to z pojazdu pozostają niemalże same koła, model zaklasyfikował pojazd do klasy: "wreck".

Zadanie #3¶

Ponownie zmodyfikuj oryginalny obraz, ale tym razem zaszumiając go w losowy sposób (przykładowa implementacja: https://www.geeksforgeeks.org/add-a-salt-and-pepper-noise-to-an-image-with-python/). Czy najbardziej prawdopodobna klasa zmienia się wraz ze zmianą szumu? Przetestuj dla każdego z modeli.

In [68]:
import random 
import cv2 
  
def add_noise(img): 
  
    # Getting the dimensions of the image 
    row , col = img.shape 
      
    # Randomly pick some pixels in the 
    # image for coloring them white 
    # Pick a random number between 300 and 10000 
    number_of_pixels = random.randint(300, 10000) 
    for i in range(number_of_pixels): 
        
        # Pick a random y coordinate 
        y_coord=random.randint(0, row - 1) 
          
        # Pick a random x coordinate 
        x_coord=random.randint(0, col - 1) 
          
        # Color that pixel to white 
        img[y_coord][x_coord] = 255
          
    # Randomly pick some pixels in 
    # the image for coloring them black 
    # Pick a random number between 5000 and 10000 
    number_of_pixels = random.randint(5000 , 10000) 
    for i in range(number_of_pixels): 
        
        # Pick a random y coordinate 
        y_coord=random.randint(0, row - 1) 
          
        # Pick a random x coordinate 
        x_coord=random.randint(0, col - 1) 
          
        # Color that pixel to black 
        img[y_coord][x_coord] = 0
          
    return img 
  
# salt-and-pepper noise can 
# be applied only to grayscale images 
# Reading the color image in grayscale image 
img = cv2.imread('./data/amfibia.jpg', 
                 cv2.IMREAD_GRAYSCALE) 
  
#Storing the image 
cv2.imwrite('./data/sp_amfibia.jpg', 
            add_noise(img)) 
Out[68]:
True
In [69]:
sp_amphibious_vehicle = get_image("./data/sp_amfibia.jpg")
In [70]:
explanation_sp_amhibious_vehicle_inception_v3 = explainer.explain_instance(
    image=lime_transformer(sp_amphibious_vehicle), 
    classifier_fn=partial(predict_batch, inception_v3),
    top_labels=5,
    num_samples=1000)

image, mask = explanation_sp_amhibious_vehicle_inception_v3.get_image_and_mask(
    label=explanation_sp_amhibious_vehicle_inception_v3.top_labels[0],
    positive_only=False,
    negative_only=False,
    num_features=10,
    hide_rest=False)
boundaries = mark_boundaries(image, mask)
plt.imshow(boundaries)
100%|██████████| 1000/1000 [00:26<00:00, 37.10it/s]
Out[70]:
<matplotlib.image.AxesImage at 0x36530aae0>
No description has been provided for this image
In [71]:
for index in explanation_sp_amhibious_vehicle_inception_v3.top_labels:
    print(index_to_label[index])
amphibian
jeep
tow_truck
car_wheel
half_track
In [72]:
explanation_sp_amhibious_vehicle_mobilenet_v3_l = explainer.explain_instance(
    image=lime_transformer(sp_amphibious_vehicle), 
    classifier_fn=partial(predict_batch, mobilenet_v3_l),
    top_labels=5,
    num_samples=1000)

image, mask = explanation_sp_amhibious_vehicle_mobilenet_v3_l.get_image_and_mask(
    label=explanation_sp_amhibious_vehicle_mobilenet_v3_l.top_labels[0],
    positive_only=False,
    negative_only=False,
    num_features=10,
    hide_rest=False)
boundaries = mark_boundaries(image, mask)
plt.imshow(boundaries)
100%|██████████| 1000/1000 [00:28<00:00, 35.45it/s]
Out[72]:
<matplotlib.image.AxesImage at 0x3650748f0>
No description has been provided for this image
In [73]:
for index in explanation_sp_amhibious_vehicle_mobilenet_v3_l.top_labels:
    print(index_to_label[index])
tow_truck
amphibian
snowplow
racer
jeep
In [74]:
explanation_sp_amhibious_vehicle_resnet50 = explainer.explain_instance(
    image=lime_transformer(sp_amphibious_vehicle), 
    classifier_fn=partial(predict_batch, resnet50),
    top_labels=5,
    num_samples=1000)

image, mask = explanation_sp_amhibious_vehicle_resnet50.get_image_and_mask(
    label=explanation_sp_amhibious_vehicle_resnet50.top_labels[0],
    positive_only=False,
    negative_only=False,
    num_features=10,
    hide_rest=False)
boundaries = mark_boundaries(image, mask)
plt.imshow(boundaries)
100%|██████████| 1000/1000 [00:30<00:00, 32.62it/s]
Out[74]:
<matplotlib.image.AxesImage at 0x3650bc8f0>
No description has been provided for this image
In [75]:
for index in explanation_sp_amhibious_vehicle_resnet50.top_labels:
    print(index_to_label[index])
jeep
amphibian
Model_T
pickup
tow_truck

Po zaszumieniu oryginalnego obrazu możemy zauważyć, że tylko model InceptionV3 poradził sobie z właściwą klasyfikacją pojazdu. Model MobileNetV3 wskazał błędnie "tow_truck", natomiast ResNet-50 "jeep". Obydwa modele wskazały amfibię na drugim miejscu.

Słabszy wynik w przypadku MobileNet oraz ResNet nie powinien dziwić, gdyż są to modele słabiej klasyfikujące obiekty na obrazku, niż InceptionV3.

Poniżej przeprowadzę ponowne zaszumienie obrazu i zobaczymy, czy tym razem InceptionV3 sobie poradzi.

In [94]:
img = cv2.imread('./data/sp4_amfibia.jpg', 
                 cv2.IMREAD_GRAYSCALE) 
  
#Storing the image 
cv2.imwrite('./data/sp5_amfibia.jpg', 
            add_noise(img)) 
Out[94]:
True
In [95]:
sp_amphibious_vehicle = get_image("./data/sp5_amfibia.jpg")
In [96]:
explanation_sp_amhibious_vehicle_inception_v3 = explainer.explain_instance(
    image=lime_transformer(sp_amphibious_vehicle), 
    classifier_fn=partial(predict_batch, inception_v3),
    top_labels=5,
    num_samples=1000)

image, mask = explanation_sp_amhibious_vehicle_inception_v3.get_image_and_mask(
    label=explanation_sp_amhibious_vehicle_inception_v3.top_labels[0],
    positive_only=False,
    negative_only=False,
    num_features=10,
    hide_rest=False)
boundaries = mark_boundaries(image, mask)
plt.imshow(boundaries)
100%|██████████| 1000/1000 [00:27<00:00, 35.88it/s]
Out[96]:
<matplotlib.image.AxesImage at 0x36593a7b0>
No description has been provided for this image
In [97]:
for index in explanation_sp_amhibious_vehicle_inception_v3.top_labels:
    print(index_to_label[index])
amphibian
half_track
tow_truck
pickup
jeep

Dwukrotne zaszumienie obrazu nie pogorszyło jakości klasyfikacji modelu InceptionV3. Trzy- i czterokrotnie również nie. W tym momencie przerywam ten dodatkowy eksperyment. Ewidentnie InceptionV3 jako jeden z najmocniejszych modeli do rozpoznawania obiektów na obrazie radzi sobie z takim zaszumieniem perfekcyjnie.

Podsumowanie¶

Przedstawione powyżej przypadki w bardzo dobry sposób przedstawiają przydatność narzędzia LIME do analizowania istotności superpikseli dla jakości klasyfikacji.
Analiza ta pozwala w kontrolowany sposób porównać ze sobą różne modele i ujawnić ich mocne oraz słabe strony.
Przede wszystki narzędzie pokazuje "ludzką" stronę modeli, gdyż widząc relewantne superpiksele, sami możemy przytaknąć, że w duże mierze faktycznie pokrywają się z cechami obiektów, po których my jako ludzie rozróżniamy przedmioty.